'use strict'

/**
 * 这是一个示例程序，用于展示如何完成一个微型的HTTP服务框架。
 */

const http = require('http')
const url = require('url')

/* ------------------------中间件实现-------------------------------- */

class middleware {
  constructor (options = {}) {
    this.chain = []
    this.stack = []
    this.cache = false

    this.init()

    if (options && typeof options === 'object') {
      if (options.cache !== undefined) {
        this.cache = options.cache
      }
    }

  }

  init () {
    this.chain = []
    let first = async (ctx) => {
      if (ctx && typeof ctx === 'object' && ctx.exec && typeof ctx.exec === 'function') {
        return await ctx.exec(ctx)
      }
    }

    this.chain.push(first)
  }

  add (midcall, filter = null) {
    if (typeof midcall !== 'function' || midcall.constructor.name !== 'AsyncFunction') {
      throw new Error(`middleware must be a async function`)
    }

    let last = this.chain.length - 1
    let nextcall = this.chain[last]

    let realmid = function () {
      if (filter !== null && typeof filter === 'function') {
        return async (ctx) => {
          if ( false === await filter(ctx) ) {
            return await nextcall(ctx)
          }
          return await midcall(ctx, nextcall.bind(null, ctx))
        }
      }

      return async (ctx) => {
        return await midcall(ctx, nextcall.bind(null, ctx))
      }

    }

    this.chain.push(realmid())
  }

  loadstack () {
    this.init()
    for (let i = this.stack.length - 1; i >= 0; i--) {
      this.add(
        this.stack[i].midcall,
        this.stack[i].filter
      )
    }
  }

  use (midcall, filter = null) {

    this.stack.push({
      midcall: midcall,
      filter:  filter
    })

    if (this.cache === false) {
      this.loadstack()
    }
  }

  async run (ctx) {
    let last = this.chain.length - 1
    return await this.chain[last](ctx)
  }

}

/** -------------------------中间件实现 - END ------------------------ */

/** --------------------------路由模块 ------------------------------- */

/**
 * 路由模块不支持参数解析，目的在于展示核心功能，不引入过多复杂的代码。
 */

class router {
  constructor () {

    this.apiTable = {
      GET     : {},
      POST    : {},
      PUT     : {},
      DELETE  : {},
      OPTIONS : {},
      PATCH   : {},
      HEAD    : {},
      TRACE   : {}
    }

    this.methods = Object.keys(this.apiTable)

    //生成各个请求类型的小写函数。
    for (let m of this.methods) {
      this[m.toLowerCase()] = function (path, callback) {
        this.addPath(m, path, callback)
      }
    }

  }
  /**
   * 格式化路由，多个/合并成一个，开头必须是/，末尾的/去掉。
   * @param {string} path 
   */
  fmtPath (path) {
    if (path.length == 0) {
      return '/'
    }
    
    path = path.replace(/\/\/+/g, '/')

    if (path[0] !== '/') path = `/${path}`

    if (path.length > 1 && path[path.length - 1] === '/')
      path = path.substring(0, path.length - 1)

    return path
  }

  addPath (method, path, callback) {
    if (!this.apiTable[method]) {
      throw new Error(`不支持的请求类型：${method}。`)
    }

    if (typeof callback !== 'function' || callback.constructor.name !== 'AsyncFunction') {
      throw new Error('请求执行函数必须使用async声明。')
    }

    let real_path = this.fmtPath(path)

    if (this.apiTable[method][real_path]) {
      throw new Error('路由已经存在。')
    }

    this.apiTable[method][real_path] = callback

  }

  find (method, path) {
    
    if (!this.apiTable[method]) return null

    if (path.length > 1 && path[path.length - 1] === '/')
      path = path.substring(0, path.length - 1)

    let cb = this.apiTable[method][path]

    if (!cb) return null

    return {
      path,
      callback: cb
    }

  }

}

/** --------------------------路由模块 - END ------------------------- */

/** --------------------------context模块---------------------------- */

/**
 * 请求上下文模块用于生成请求上下文对象，并打包request和response
 */

class context {
  constructor () {
    this.method = ''
    this.version = '1.1'

    this.rawBody = null

    this.url = ''

    this.host = ''

    this.port = 0

    this.path = ''

    this.routepath = ''

    this.body = null

    this.query = null
    
    this.resBody = ''

    this.request = null
    this.response = null

    this.headers = null

    this.exec = null

  }

}

/** --------------------------context模块 - END --------------------- */

class httpserver {

  constructor () {
    this.midware = new middleware()
    this.use = this.midware.use.bind(this.midware)
    this.router = new router()
    
    let mc
    for (let m of this.router.methods) {
      mc = m.toLowerCase()
      this[mc] = this.router[mc].bind(this.router)
    }

    this.server = null

    this.requestNotFound = 'not found'
    this.badRequest = 'bad request'
  }

  addFinalMidware () {
    this.midware.add(async (c, next) => {
      await next()
      if (!c.response.writable) {
        return
      }

      if (c.resBody)
        c.response.end(c.resBody)
      else
        c.response.end()
  
    })
  }

  async onRequest (req, res) {

    req.on('error', err => {
      req.destroy()
    })

    res.on('error', err => {
      res.destroy()
    })

    let urlobj = new url.URL(`http://${req.headers['host']}${req.url}`)

    console.log(urlobj)
    let route = this.router.find(req.method, urlobj.pathname)

    if (!route) {
      res.statusCode = 404
      return res.end(this.requestNotFound)
    }
    
    let ctx = new context()

    ctx.method = req.method
    ctx.request = req
    ctx.response = res
    ctx.headers = req.headers
    ctx.url = req.url
    ctx.path = urlobj.pathname
    ctx.host = urlobj.host
    ctx.port = urlobj.port ? parseInt(urlobj.port) : 0
    ctx.routepath = route.path
    ctx.exec = route.callback
    ctx.query = urlobj.searchParams

    try {
      await this.midware.run(ctx)
    } catch (err) {
      console.error(err)
      if (res.writable) {
        res.statusCode = 500
        res.end()
      }
    } finally {
      ctx.response = 
      ctx.request = 
      ctx.headers = 
      ctx.resBody = 
      ctx.rawBody = 
      ctx.query = 
      ctx.body = null
      ctx = null
    }
   
  }

  listen (port, host) {
    
    this.midware.cache && this.midware.loadstack()

    this.addFinalMidware()
    
    this.server = http.createServer(this.onRequest.bind(this))

    this.server.listen(port, host)

    return this.server
  }

}

module.exports = httpserver
